package util;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;

/**
 * 
 * Clase para pedir todas las clases de un lugar. La robe de internet.
 */
class StringComparator implements Comparator<String>, Serializable {

    private static final long serialVersionUID = 5603928894805771653L;

    public static final StringComparator DEFAULT = new StringComparator();

    /**
     * Compares two strings.
     **/
    @Override
    public int compare(final String pString1, final String pString2) {

        if (pString1 == pString2) { // NOPMD
            return 0;
        }

        if (pString1 == null) {// NOPMD
            return Integer.MIN_VALUE;
        }

        if (pString2 == null) {// NOPMD
            return Integer.MAX_VALUE;
        }

        if (pString1.equals(pString2)) {
            return 0;
        }

        final int aS1Length = pString1.length();
        final int aS2Length = pString2.length();
        final int aMinLength = Math.min(aS1Length, aS2Length);
        for (int i = 0; i < aMinLength; i++) {
            char aC1 = pString1.charAt(i);
            char aC2 = pString2.charAt(i);

            if (aC1 != aC2) {
                return aC1 - aC2;
            }
        }

        final int aLenghtDiffer = aS1Length - aS2Length;
        return aLenghtDiffer;// NOPMD
    }

}

class ClassComparator implements Comparator<Class<?>>, Serializable {

    private static final long serialVersionUID = -7120534646583185358L;

    public static final ClassComparator DEFAULT = new ClassComparator();

    /** Compares two classes. */
    @Override
    public int compare(final Class<?> pClass1, final Class<?> pClass2) {

        if (pClass2 == null) {
            return Integer.MAX_VALUE;
        }
        if (pClass1 == null) {
            return Integer.MIN_VALUE;
        }
        if (pClass1.equals(pClass2)) {
            return 0;
        }

        final String aC1Name = pClass1.getCanonicalName();
        final String aC2Name = pClass2.getCanonicalName();
        return StringComparator.DEFAULT.compare(aC1Name, aC2Name);
    }

}

public class ClassDiscovery {// NOPMD

    private static String getClassNameafterPackageAsPath(final String pFileName, final String pPkgAsPath) {
        final String cName = pFileName.substring(0, pFileName.length() - 6).replace('/', '.').replace('\\', '.');
        return cName.substring(pPkgAsPath.length());
    }

    private ClassDiscovery() {
        // empty constructor y privado
    }

    private static String getClassNameOfPackageAsPath(final String pFileName, final String pPkgAsPath) {

        final boolean aIsClass = pFileName.endsWith(".class");
        if (!aIsClass) {
            return null;
        }

        final boolean aIsBelongToPackage = pFileName.startsWith(pPkgAsPath);
        if (!aIsBelongToPackage) {
            return null;
        }

        return ClassDiscovery.getClassNameafterPackageAsPath(pFileName, pPkgAsPath);
    }

    private static File getPackageFile(final String pPkgName, final File pPkgPath) {// NOPMD

        final String aPkgFileName = pPkgPath.getAbsoluteFile().toString() + '/' + pPkgName.replace('.', '/');
        final File aPkgFile = new File(aPkgFileName);

        final boolean aIsExist = aPkgFile.exists();
        final boolean aIsDirectory = aPkgFile.isDirectory();
        final boolean aIsExistasDirectory = aIsExist && aIsDirectory;
        if (!aIsExistasDirectory) {
            return null;
        }

        return aPkgFile;
    }

    private static boolean checkIsJarFile(final File pFile) {
        return pFile.toString().endsWith(".jar");
    }

    private static List<String> discoverClassNamesfromJarFile(final PkgInfo pPkgInfo) {

        final ArrayList<String> aClassNames = new ArrayList<String>();
        try {
            final JarFile jF = new JarFile(pPkgInfo.pkgPath);
            final Enumeration<JarEntry> jEs = jF.entries();

            while (jEs.hasMoreElements()) {
                final JarEntry aJE = jEs.nextElement();
                final String aJEName = aJE.getName();

                final String aSimpleName = getClassNameOfPackageAsPath(aJEName, pPkgInfo.pkgAsPath);
                if (aSimpleName == null) {
                    continue;
                }

                final String aClassName = pPkgInfo.pkgName + '.' + aSimpleName;
                aClassNames.add(aClassName);
            }

            jF.close();
        } catch (IOException e) {// NOPMD
        }

        return aClassNames;
    }

    private static void discoverClassNamesfromDirectory(final String pAbsolutePackagePath, final String pPackageName,
            final File pPackageFolder, final List<String> pClassNames) {
        final File[] aFiles = pPackageFolder.listFiles();
        for (File aFile : aFiles) {
            if (aFile.isDirectory()) {
                discoverClassNamesfromDirectory(pAbsolutePackagePath, pPackageName, aFile, pClassNames);
                continue;
            }

            final String aFileName = aFile.getAbsolutePath().substring(pAbsolutePackagePath.length() + 1);
            final boolean aIsClassFile = aFileName.endsWith(".class");
            if (!aIsClassFile) {
                continue;
            }

            final String aSimpleName = aFileName.substring(0, aFileName.length() - 6).replace('/', '.')
                    .replace('\\', '.');
            final String aClassName = pPackageName + '.' + aSimpleName;
            pClassNames.add(aClassName);
        }
    }

    private static List<String> discoverClassNamesfromDirectory(final PkgInfo pPkgInfo) {

        final ArrayList<String> aClassNames = new ArrayList<String>();
        final File aPkgFile = ClassDiscovery.getPackageFile(pPkgInfo.pkgName, pPkgInfo.pkgPath);
        if (aPkgFile == null) {
            return aClassNames;
        }

        discoverClassNamesfromDirectory(aPkgFile.getAbsolutePath(), pPkgInfo.pkgName, aPkgFile, aClassNames);
        return aClassNames;
    }

    public static class PkgInfo {
        PkgInfo(final File pPkgPath, final String pPkgName, final String pPkgAsPath) {

            this.pkgPath = pPkgPath;
            this.pkgName = pPkgName;
            this.pkgAsPath = pPkgAsPath;
        }

        private File pkgPath;

        public File getPkgPath() {
            return this.pkgPath;
        }

        public void setPkgPath(final File pkgPath) {
            this.pkgPath = pkgPath;
        }

        public String getPkgName() {
            return this.pkgName;
        }

        public void setPkgName(final String pkgName) {
            this.pkgName = pkgName;
        }

        public String getPkgAsPath() {
            return this.pkgAsPath;
        }

        public void setPkgAsPath(final String pkgAsPath) {
            this.pkgAsPath = pkgAsPath;
        }

        private String pkgName;

        private String pkgAsPath;
    }

    public static PkgInfo getPackageInfoOf(final Class<?> pClass) {
        File aPkgPath = null;
        String aPkgName = null;
        String aPkgAsPath = null;

        try {
            aPkgPath = new File(pClass.getProtectionDomain().getCodeSource().getLocation().toURI());
            aPkgName = pClass.getPackage().getName();
            aPkgAsPath = aPkgName.replace('.', '/') + '/';
        } catch (Throwable e) {// NOPMD
        }

        if (aPkgPath == null) {
            return null;
        }

        return new PkgInfo(aPkgPath, aPkgName, aPkgAsPath);
    }

    public static List<String> discoverClassNamesinPackage(final PkgInfo pPkgInfo) {

        if (pPkgInfo == null) {
            return null;
        }

        List<String> aClassNames = new ArrayList<String>();
        if (pPkgInfo.pkgPath.isDirectory()) {

            aClassNames = ClassDiscovery.discoverClassNamesfromDirectory(pPkgInfo);

        } else if (pPkgInfo.pkgPath.isFile()) {
            boolean aIsJarFile = ClassDiscovery.checkIsJarFile(pPkgInfo.pkgPath);
            if (!aIsJarFile) {
                return null;
            }

            aClassNames = ClassDiscovery.discoverClassNamesfromJarFile(pPkgInfo);
        }

        return aClassNames;
    }

    /**
     * Returns an array of class in the same package as the the SeedClass
     * 
     * @param pFilterName
     *            - Regular expression to match the desired classes' name (nullif no filtering needed)
     * @param pFilterClass
     *            - The super class of the desired classes (null if no filtering needed)
     * 
     * @return - The array of matched classes, null if there is a problem.
     * 
     * @author The rest - Nawaman http://nawaman.net
     * @author Package as Dir - Jon Peck http://jonpeck.com (adapted from
     *         http://www.javaworld.com/javaworld/javatips/jw-javatip113.html)
     */
    public static <T> Class<? extends T>[] discoverClasses(final Class<?> pSeedClass, final String pFilterName,// NOPMD
            final Class<T> pFilterClass) {

        final Pattern aClsNamePattern = pFilterName == null ? null : Pattern.compile(pFilterName);

        PkgInfo aPkgInfo = null;

        try {
            aPkgInfo = getPackageInfoOf(pSeedClass);
        } catch (Throwable e) {// NOPMD
        }

        if (aPkgInfo == null) {
            return null;
        }

        final List<String> aClassNames = discoverClassNamesinPackage(aPkgInfo);

        if (aClassNames == null) {
            return null;
        }

        if (aClassNames.isEmpty()) {
            return null;
        }

        final ArrayList<Class<?>> aClasses = new ArrayList<Class<?>>();
        for (final String aClassName : aClassNames) {

            if (aClsNamePattern != null && !aClsNamePattern.matcher(aClassName).matches()) {
                continue;
            }

            // Get the class and filter it
            Class<?> aClass = null;
            try {
                aClass = Class.forName(aClassName);
            } catch (ClassNotFoundException e) {
                continue;
            } catch (NoClassDefFoundError e) {
                continue;
            }

            if (pFilterClass != null && !pFilterClass.isAssignableFrom(aClass)) {
                continue;
            }

            if (pFilterClass != null) {// NOPMD
                if (!pFilterClass.isAssignableFrom(aClass)) {
                    continue;
                }

                aClasses.add(aClass.asSubclass(pFilterClass));
            } else {
                aClasses.add(aClass);
            }
        }

        Collections.sort(aClasses, ClassComparator.DEFAULT);
        @SuppressWarnings("unchecked")
        Class<? extends T>[] aClassesArray = aClasses.toArray((Class<? extends T>[]) new Class[aClasses.size()]);

        return aClassesArray;
    }

}
